﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Media.Imaging;
using System.Windows.Interactivity;
using System.Windows;
using System.ComponentModel;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Controls;
using System.Windows.Media.Animation;

namespace WpfWordsLearner
{
	/// <summary>
	/// This changes the window's theme to the given theme URI.
	/// </summary>
	public class ChangeThemeAction : TriggerAction<Window>
	{
		///<summary>
		/// Theme URI to apply to the application
		///</summary>
		public readonly DependencyProperty ThemeUriProperty =
			DependencyProperty.Register("ThemeUri", typeof(Uri), typeof(ChangeThemeAction));

		/// <summary>
		/// Theme URI to apply to the application.
		/// </summary>
		[TypeConverter(typeof(UriTypeConverter))]
		public Uri ThemeUri
		{
			get { return (Uri)GetValue(ThemeUriProperty); }
			set { SetValue(ThemeUriProperty, value); }
		}

		/// <summary>
		/// This event is raised when the theme is changed.
		/// </summary>
		public event EventHandler ThemeChanged;

		/// <summary>
		/// Invokes the action.
		/// </summary>
		/// <param name="parameter">The parameter to the action. If the Action does not require a parameter, the parameter may be set to a null reference.</param>
		protected override void Invoke(object parameter)
		{
			// See if we were passed a parameter indicating whether to run our action or reverse the action.
			// Assume to run if not supplied.
			bool isThemeEnabled;
			if (parameter == null || !Boolean.TryParse(parameter.ToString(), out isThemeEnabled))
				isThemeEnabled = true;

			// Reversing theme? Remove resources.
			if (!isThemeEnabled)
			{
				AssociatedObject.Resources.MergedDictionaries.Clear();
				return;
			}

			// Running action -- no theme supplied? Nothing to do.
			if (ThemeUri == null)
				return;

			// See if the element we're applied to is loaded.  If not, we can't do our theme switch
			// here, instead we need to wait until it's loaded.
			if (!AssociatedObject.IsLoaded)
			{
				// Hook up a temporary handler
				RoutedEventHandler waitForLoad = null;
				waitForLoad = (s, e) =>
				{
					AssociatedObject.Loaded -= waitForLoad;
					Invoke(isThemeEnabled);
				};
				AssociatedObject.Loaded += waitForLoad;

				return;
			}

			// Apply the theme.
			try
			{
				ResourceDictionary rd = Application.LoadComponent(ThemeUri) as ResourceDictionary;
				if (rd != null)
				{
					// See if we have content, and it's a UIElement type -- if so, we will try to gradually
					// fade the content for a nice effect.  We also require an Adorner Layer to be present
					// in the Window's control template
					UIElement elementContent = AssociatedObject.Content as UIElement;
					AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(elementContent ?? AssociatedObject);
					if (elementContent == null || adornerLayer == null)
					{
						// No layer, just replace the theme.
						AssociatedObject.Resources.MergedDictionaries.Clear();
						AssociatedObject.Resources.MergedDictionaries.Add(rd);
						return;
					}

					int width, height;
					if (elementContent is FrameworkElement)
					{
						FrameworkElement fe = (FrameworkElement)elementContent;
						width = (int)fe.ActualWidth;
						height = (int)fe.ActualHeight;
					}
					else
					{
						width = (int)(AssociatedObject.ActualWidth - SystemParameters.BorderWidth * 2);
						height = (int)(AssociatedObject.ActualHeight - SystemParameters.CaptionHeight);
					}

					// Capture a bitmap of the current screen.
					var currentContent = (Visual)VisualTreeHelper.GetChild(AssociatedObject, 0);
					RenderTargetBitmap rtb = new RenderTargetBitmap(width, height, 96, 96, PixelFormats.Pbgra32);
					rtb.Render(currentContent);

					var staticImage = new Image { Source = rtb, Stretch = Stretch.None, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Top };
					var imageAdorner = new FrameworkElementAdorner(elementContent, staticImage);
					adornerLayer.Add(imageAdorner);

					// Merge the resources in.
					AssociatedObject.Resources.MergedDictionaries.Clear();
					AssociatedObject.Resources.MergedDictionaries.Add(rd);

					// Now animate the opacity of the image out so we can see the real controls
					// underneath, then drop the image off.
					DoubleAnimation doubleAnimation = new DoubleAnimation(1, 0, new Duration(TimeSpan.FromMilliseconds(1500)));
					doubleAnimation.Completed += (s, e) =>
					{
						adornerLayer.Remove(imageAdorner);
						RaiseThemeChanged();
					};
					staticImage.BeginAnimation(UIElement.OpacityProperty, doubleAnimation);
				}
			}
			catch
			{
			}
		}

		/// <summary>
		/// Raises the ThemeChanged event
		/// </summary>
		private void RaiseThemeChanged()
		{
			EventHandler handler = ThemeChanged;
			if (handler != null)
				handler(this, EventArgs.Empty);
		}
	}

	/// <summary>
	/// Simple Adorner to present a single FrameworkElement child in the adorner layer
	/// </summary>
	public class FrameworkElementAdorner : Adorner, IDisposable
	{
		private readonly FrameworkElement _child;

		/// <summary>
		/// Gets the number of visual child elements within this element.
		/// </summary>
		/// <returns>
		/// The number of visual child elements for this element.
		/// </returns>
		protected override int VisualChildrenCount
		{
			get { return 1; }
		}

		/// <summary>
		/// Overrides <see cref="M:System.Windows.Media.Visual.GetVisualChild(System.Int32)"/>, and returns a child at the specified index from a collection of child elements. 
		/// </summary>
		/// <returns>
		/// The requested child element. This should not return null; if the provided index is out of range, an exception is thrown.
		/// </returns>
		/// <param name="index">The zero-based index of the requested child element in the collection.</param>
		protected override Visual GetVisualChild(int index)
		{
			return _child;
		}

		/// <summary>
		/// Implements any custom measuring behavior for the adorner.
		/// </summary>
		/// <returns>
		/// A <see cref="T:System.Windows.Size"/> object representing the amount of layout space needed by the adorner.
		/// </returns>
		/// <param name="constraint">A size to constrain the adorner to.</param>
		protected override Size MeasureOverride(Size constraint)
		{
			_child.Measure(constraint);
			return _child.DesiredSize;
		}

		/// <summary>
		/// When overridden in a derived class, positions child elements and determines a size for a <see cref="T:System.Windows.FrameworkElement"/> derived class. 
		/// </summary>
		/// <returns>
		/// The actual size used.
		/// </returns>
		/// <param name="finalSize">The final area within the parent that this element should use to arrange itself and its children.</param>
		protected override Size ArrangeOverride(Size finalSize)
		{
			_child.Arrange(new Rect(finalSize));
			return finalSize;
		}

		///<summary>
		/// Constructor
		///</summary>
		///<param name="adornedElement">Element being adorned</param>
		///<param name="child">Child element to place in adorner layer</param>
		public FrameworkElementAdorner(UIElement adornedElement, FrameworkElement child)
			: base(adornedElement)
		{
			_child = child;
			AddVisualChild(_child);
		}

		/// <summary>
		/// This static method creates and places the adorner - it is returned so it may
		/// be disposed when not needed anymore.
		/// </summary>
		/// <param name="adornedElement">Element being adorned</param>
		/// <param name="child">Child element to place in adorner layer</param>
		/// <returns>Created adorner</returns>
		public static FrameworkElementAdorner CreateElementAdorner(UIElement adornedElement, FrameworkElement child)
		{
			AdornerLayer layer = AdornerLayer.GetAdornerLayer(adornedElement);
			if (layer != null)
			{
				var elementAdorner = new FrameworkElementAdorner(adornedElement, child);
				layer.Add(elementAdorner);
				return elementAdorner;
			}
			// No layer present - possibly too early in app cycle, or layer missing from
			// visual tree.
			return null;
		}

		#region Implementation of IDisposable

		/// <summary>
		/// Removes the adorner from the layer.
		/// </summary>
		public void Dispose()
		{
			try
			{
				var layer = AdornerLayer.GetAdornerLayer(AdornedElement);
				if (layer != null)
					layer.Remove(this);
			}
			catch
			{
			}
		}

		#endregion
	}
}
